Redis分布式锁的实现(Jedis和Redisson两个方案)
来源:https://my.oschina.net/u/3452433/blog/3004997/print
应用场景
分布式锁主要用于解决,公司中不同业务系统对同一功能的数据产生脏读或重复插入。
比如公司现有三个小组分别开发WAP站、小程序、APP客户端,而这三个系统都存在领红包功能。
业务要求每人每日只能领取一个红包,如果有人同时登陆三个系统那么就能够同一时间领取到三个红包。
分布式锁的要求
分布式锁要满足以下基本要求:
「共享锁」多系统能够共享同一个锁机制。 「互斥性」在任意时刻,只有一个请求能持有锁。 「无死锁」在程序崩溃时能够,自动释放锁。 「持有者解锁」锁只能被加锁的请求解锁,其他请求无法解锁。
Jedis实现分布式锁
本例参考了博文:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/
例子已上传码云:https://gitee.com/imlichao/jedis-distributed-lock-example
添加依赖
本例使用spring boot提供的redis实现,并没有直接引入jedis依赖。这样做的好处是,可以在项目中同时使用Jedis和RedisTemplate实例。
pom.xml文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
配置文件
application.properties文件
#redis
spring.redis.database=0
spring.redis.host=18.6.8.22
spring.redis.password=Mfqy_redis_password_
spring.redis.port=6379
#连接超时时间(项目或连接池链接redis超时的时间)
spring.redis.timeout=2000
#最大连接数(建议为 业务期望QPS/单个连接的QPS,50000/1000=50)
spring.redis.pool.max-active=50
#最大空闲链接数(为减小伸缩产生的性能消耗,建议和最大连接数设成一致的)
spring.redis.pool.max-idle=50
#最小空闲连接数(0代表在无请求的状况下从不创建链接)
spring.redis.pool.min-idle=0
#连接池占满后无法获取连接时的阻塞时间(超时后抛出异常)
spring.redis.pool.max-wait=3000
Jedis工厂类
由于我们使用了spring boot提供的redis实现,所以我们不能直接获取到jedis对象。Jedis工厂类从RedisConnectionFactory中获取Redis连接(JedisConnection实现类),然后使用反射的方法从中取得了Jedis实例。
/**
* Jedis工厂类(单例模式)
*/
@Service
public class JedisFactory {
@Autowired
private RedisConnectionFactory connectionFactory;
private JedisFactory(){}
private static Jedis jedis;
/**
* 获得jedis对象
*/
public Jedis getJedis() {
//从RedisConnectionFactory中获取Redis连接(JedisConnection实现类),然后使用反射的方法从中取得了Jedis实例
if(jedis == null){
Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
ReflectionUtils.makeAccessible(jedisField);
jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection());
}
return jedis;
}
}
为避免死锁的发生,加锁和设定失效时间必须是一个原子性操作。否则一旦在加锁后程序出错,没能够执行设置失效时间的方法时,就会产生死锁。但是RedisTemplate屏蔽了插入数据和设置失效时间同时执行的方法,我们只能获取到Jedis实例来执行。
分布式锁实现类
分布式锁主要实现了两个方法即占用锁和释放锁。
这里需要注意占用锁和释放锁都要保证原子性操作,避免程序异常时产生死锁。
锁id主要用于标识持有锁的请求,在释放琐时用来判断只有持有正确锁id的请求才能执行解锁操作。
/**
* redis分布式锁
*/
@Service
public class DistributedLock {
@Autowired
private JedisFactory JedisFactory;
/**
* 占用锁
* @param lockKey 锁key
* @return 锁id
*/
public String occupyDistributedLock(String lockKey) {
//获得jedis实例
Jedis jedis = JedisFactory.getJedis();
//锁id(必须拥有此id才能释放锁)
String lockId = UUID.randomUUID().toString();
//占用锁同时设置失效时间
String isSuccees = jedis.set(lockKey, lockId, "NX","PX", 15000);
//占用锁成功返回锁id,否则返回null
if("OK".equals(isSuccees)){
return lockId;
}else{
return null;
}
}
/**
* 释放锁
* @param lockKey 锁key
* @param lockId 加锁id
*/
public void releaseDistributedLock(String lockKey,String lockId) {
if(lockId != null){
//获得jedis实例
Jedis jedis = JedisFactory.getJedis();
//执行Lua代码删除lockId匹配的锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockId));
}
}
}
解释一下jedis.set(lockKey, lockId, "NX","PX", 15000)方法
格式 - String set(String key, String value, String nxxx, String expx, long time); 功能 - 存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。参数 - key :redis key value :redis值 nxxx:只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set expx:只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。time:过期时间,单位是expx所代表的单位。
测试代码
「Controller」
/**
* 分布式锁测试类
*/
@Controller
public class DistributedLockController {
@Autowired
private DistributedLock distributedLock;
@RequestMapping(value = "/", method = RequestMethod.GET)
public String index(){
return "/index";
}
@RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
public String occupyDistributedLock(RedirectAttributes redirectAttributes, HttpServletRequest request){
String key = "userid:55689";
String lockId = null;
try{
//占用锁
lockId = distributedLock.occupyDistributedLock(key);
if(lockId != null){
//程序执行
TimeUnit.SECONDS.sleep(10);
}
}catch (Exception e){
e.printStackTrace();
}finally {
//释放锁
distributedLock.releaseDistributedLock(key,lockId);
}
redirectAttributes.addFlashAttribute("lockId",lockId);
return "redirect:/";
}
}
「页面」
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>distributed lock</title>
</head>
<body>
<h1>分布式锁测试</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button>
<br/>
<!-- 占用成功返回锁id -->
<#if lockId??>${lockId}</#if>
</body>
</html>
Redisson实现分布式锁(推荐)
使用Redisson提供的分布式锁更加方便,而且锁的具体细节也不需要考虑。
例子已上传码云:https://gitee.com/imlichao/redisson-distributed-lock-example
官网:https://redisson.org/
文档:https://github.com/redisson/redisson/wiki
SpringBoot配置
「添加依赖」
spring boot 中引用专用依赖,会自动生成配置和spring bean的实例。
pom.xml文件
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.1</version>
</dependency>
「配置文件」
application.properties文件
#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379
「测试代码」
「Controller」
/**
* 分布式锁测试类
*/
@Controller
public class DistributedLockController {
@Autowired
private RedissonClient redisson ;
@RequestMapping(value = "/", method = RequestMethod.GET)
public String index(){
return "/index";
}
@RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
public String occupyDistributedLock(RedirectAttributes redirectAttributes){
RLock lock = null;
try{
//锁的key
String key = "MF:DISTRIBUTEDLOCK:S:personId_1001";
//获得分布式锁实例
lock = redisson.getLock(key);
//加锁并且设置自动失效时间15秒
lock.lock(15, TimeUnit.SECONDS);
//程序执行
TimeUnit.SECONDS.sleep(10);
//获取网络时间(多服务器测试统一时间)
URL url=new URL("http://www.baidu.com");
URLConnection conn=url.openConnection();
conn.connect();
long dateL=conn.getDate();
Date date=new Date(dateL);
//打印和返回结果
System.out.println(date);
redirectAttributes.addFlashAttribute("success",date);
}catch (Exception e){
e.printStackTrace();
}finally {
//释放锁
if (lock != null) lock.unlock();
}
return "redirect:/";
}
}
「页面」
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>distributed lock</title>
</head>
<body>
<h1>分布式锁测试</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button>
<br/>
<#if success??>${success?datetime}</#if>
</body>
</html>
手动配置
「添加依赖」
pom.xml文件
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.1</version>
</dependency>
「配置文件」
application.properties文件
#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379
「配置类」
/**
* Redisson配置
*/
@Configuration
public class RedissonConfig {
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private String port;
@Bean
RedissonClient createConfig() {
Config config = new Config();
//设置编码方式为 Jackson JSON 编码(不设置默认也是这个)
config.setCodec(new JsonJacksonCodec());
//云托管模式设置(我们公司用的阿里云redis产品)
config.useReplicatedServers()
//节点地址设置
.addNodeAddress("redis://"+host+":"+port)
//密码
.setPassword(password)
//数据库编号(默认0)
.setDatabase(database);
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
「测试代码与SpringBoot配置一样」
精彩推荐
欢迎长按下图关注公众号后端技术精选